// Copyright (c) Microsoft Corporation.
// 
// Author: Vance Morrison
// Date: 4/2007

using System;
using Extensibility;
using EnvDTE;
using EnvDTE80;
using Microsoft.VisualStudio.CommandBars;
using System.Collections.Generic;
using System.IO;

// See code:#Overview for overview.  
namespace VSAddins
{
    /// <summary>
    /// #Overview
    /// 
    /// VSInterface implements a framework for making a Visual Studio addin. Basically to implement an addin,
    /// simply inherit from VSInterface and define the code:VSInterface.Initialize method, to define user
    /// interface for your new commands using the code:VSInterface.AddCommand. VSInterface hides a good chunk
    /// of the complexity associated with interfacing to Visual Studio.
    /// </summary>
    public abstract class VSInterface : IDTExtensibility2, IDTCommandTarget, IDisposable
    {
        public VSInterface()
        {
            addinCommands = new Dictionary<string, CommandInfo>();
        }

        // Initialize is called when the Addin is loaded for the very first time.  It is only called once. 
        // The Addin is responsible for creating 'CommandInfo' structures that describe the UI for any
        // commands and then calling the 'AddCommand' to create them.  
        abstract public void Initialize(DTE2 dte, bool UIInit);

        /// <summary>
        /// The DTE (Design Time Extentions) object is the 'root' object of the entire Visual Studio object
        /// model.  
        /// </summary>
        protected DTE2 DTE
        {
            get { return dte; }
            set { dte = value; }
        }

        /// <summary>
        /// AddCommand should be called from the code:VSInterface.Initialize routine to specify a new
        /// command to add to the user interface.  see code:CommandInfo for more.  
        /// </summary>
        protected void AddCommand(CommandInfo commandInfo)
        {
            addinCommands.Add(commandInfo.Name, commandInfo);
        }

        /// <summary>
        /// Allows you to access the commands after startup.  
        /// </summary>
        protected IDictionary<string, CommandInfo> Commands { get { return addinCommands; } }

        /// <summary>
        /// Display the html help page in VS 
        /// </summary>
        protected virtual void HelpCommand()
        {
            // Unpack the html file from the resource stream and display it the browser window. 
            string tempName = Path.Combine(Path.GetTempPath(), AddinName + "Help.htm");
            Log.WriteLine("Unpacking help html to " + tempName);
            if (Utilities.UnpackResourceToFile(@".\Help.htm", tempName))
                dte.ExecuteCommand("View.WebBrowser", "\"" + "file:" + tempName + "\"");
            else
            {
                Log.WriteLine("Error: No help available");
                System.Media.SystemSounds.Beep.Play();
            }
        }

        protected string AddinName;

        /// <summary>
        /// This is responsible for create the user interface for the commands. 
        /// If 'quick' is true, then it will return early if it looks like the UI is already set up.  
        /// </summary>
        private void SetupUserInterface(bool quick)
        {
            object[] contextGUIDS = new object[] { };
            Commands2 commands = (Commands2)dte.Commands;

            CommandBars commandBars = (CommandBars)dte.CommandBars;

            // We want to add options to the code window popup, fetch its commandBar. 
            CommandBar codeWindowContextMenu = commandBars["Code Window"];
            CommandBar mainMenu = commandBars["MenuBar"];

            // Note needed today, but if you did want to put something on an existing menu here is how to do it. 
            // CommandBar toolsMenu = ((CommandBarPopup)mainMenu.Controls["Tools"]).CommandBar;

            // Create the Addin Menu item (like File, Edit, View ... Tools ...)
            if (AddinName == null)
                AddinName = "Addin";
            CommandBar addinMenu = null;
            CommandBarPopup addinMenuPopup = (CommandBarPopup)FindControl(AddinName, mainMenu.Controls);
            if (addinMenuPopup != null)
            {
                if (quick)
                {
                    string myName = this.GetType().FullName;
                    Log.WriteLine("Detected that UI is already set up, type 'start devenv /resetAddin " + myName + "' to reset");
                    return;
                }
                addinMenu = addinMenuPopup.CommandBar;
            }
            else
                addinMenu = (CommandBar)commands.AddCommandBar(AddinName, vsCommandBarType.vsCommandBarTypeMenu, mainMenu, mainMenu.Controls.Count);

            // Create the Addin CommandBar (toolbar) if it does not exist already
            string toolbarName = AddinName + " Toolbar";
            CommandBar addinToolbar = FindCommandBar(toolbarName, commandBars);
            if (addinToolbar == null)
            {
                addinToolbar = (CommandBar)commands.AddCommandBar(toolbarName, vsCommandBarType.vsCommandBarTypeToolbar, null, 1);
                addinToolbar.Visible = true;
                addinToolbar.Position = MsoBarPosition.msoBarTop;
            }

            Command command = FindCommand("View.SynchronizeClassView", commands);
            if (command != null)
            {
                if (addinToolbar.Controls.Count == 0)
                    command.AddControl(addinToolbar, addinToolbar.Controls.Count + 1);
                command.Bindings = new object[] { "Global::Alt+S" };                    // Add the shortcut.
            }

            foreach (CommandInfo commandInfo in addinCommands.Values)
            {
                command = commands.AddNamedCommand2(me, commandInfo.Name, commandInfo.ToolTip,
                    commandInfo.ToolTip, true, 1, ref contextGUIDS,
                    (int)vsCommandStatus.vsCommandStatusSupported + (int)vsCommandStatus.vsCommandStatusEnabled,
                    (int)vsCommandStyle.vsCommandStyleText, vsCommandControlType.vsCommandControlTypeButton);

                command.AddControl(addinMenu, addinMenu.Controls.Count + 1);

                if (commandInfo.KeyboardBinding != null)
                    command.Bindings = new object[] { commandInfo.KeyboardBinding };

                if (commandInfo.OnToolBar)
                    command.AddControl(addinToolbar, addinToolbar.Controls.Count + 1);

                if (commandInfo.OnContextMenu)
                    command.AddControl(codeWindowContextMenu, 1);                         // And in the right click context menu for the code window.  
            }
        }

        // IDTExtensibilty is a series of callbacks that indicate when addins come and go from VS.
        // The most important of these is code:VSInterface.OnConnection
        #region IDTExtensibility2 Implementation

        // #VSCommandModel
        // 
        // Visual Studio Command model needs a bit of explaination
        // 
        // The model is designed to avoid loading Addin DLLs that are not actually used by the user.  They do
        // this by breaking the loading of an addin into two phase.  
        // 
        //    * Querying for User interface:  This happens only the FIRST time an addin is loaded, In this phase 
        //        the code:VSInterface.OnConnection is called with the enum code:ext_ConnectMode.ext_cm_UISetup.
        //        The Addin responds by making callbacks to the DTE object to set up the user interface.  VS
        //        PERSISTS that layout and uses it to supply the UI on the next launch WITHOUT LOADING THE DLL. 
        //        It is only when a command button is clicked that the DLL is loaded.    
        //        
        //        If the user interface is messed up, it can be reset with the command 
        //        
        //                start devenv /resetAddin <addinName>
        //                
        //        Where <addinName> is the FULLY QUALIFIED class name for the addin (the FullClassName property
        //        in the *.Addin file.   This will cause VS to load the Addin again and query it for its
        //        information.  
        //        
        //    * Initializing the adding.  This is when  code:VSInterface.OnConnection is called with the enum
        //        code:ext_ConnectMode.ext_cm_Startup.  This is called once when the addin is first loaded. 
        //        Addins will be loaded when the user explicitly asks for them to be loaded (using the Addin
        //        Manager dialog), or at VS startup IF the *.Addin file specifies the CommandPreload element. 
        //        

        /// <summary>
        /// Implements the OnConnection method of the IDTExtensibility2 interface. Receives notification that
        /// the Add-in is being loaded.
        /// </summary>
        /// <param term='application'>Root object of the host application.</param>
        /// <param term='connectMode'>Describes how the Add-in is being loaded.</param>
        /// <param term='addInInst'>Object representing this Add-in.</param>
        /// <seealso class='IDTExtensibility2' />
        void IDTExtensibility2.OnConnection(object application, ext_ConnectMode connectMode, object addInInst, ref Array custom)
        {
            // I can't do the initialization in the constructor because I don't have the DTE object.  
            // Thus I have to do it here on first use.  
            if (dte == null)
            {
                dte = (DTE2)application;
                me = (AddIn)addInInst;
                myType = this.GetType();
                myTypeName = myType.FullName;
                Log.Initialize(dte, "Addin Log");
                Initialize(dte, connectMode == ext_ConnectMode.ext_cm_UISetup);
            }

            try
            {

                // UISetup happens only when the addin is first found.  After that VS uses cached information to
                // determine the GUI command layout.  You can force this to happen again by running 
                // start devenv /resetaddin HyperAddin.Connection
                if (connectMode == ext_ConnectMode.ext_cm_UISetup)
                {
                    SetupUserInterface(false);
                }
                else
                    SetupUserInterface(true);       // This is for robustness.
            }
            catch (Exception e)
            {
                Log.Enabled = true;
                Log.WriteLine("An exception was thrown during the OnConnection callback with connectMode " + connectMode + ".");
                Log.WriteLine(e.ToString());
            }

        }

        /// <summary>
        /// Implements the OnDisconnection method of the IDTExtensibility2 interface. Receives notification
        /// that the Add-in is being unloaded.
        /// </summary>
        /// <param term='disconnectMode'>Describes how the Add-in is being unloaded.</param>
        /// <param term='custom'>Array of parameters that are host application specific.</param>
        /// <seealso class='IDTExtensibility2' />
        void IDTExtensibility2.OnDisconnection(ext_DisconnectMode disconnectMode, ref Array custom)
        {
            this.Dispose();
        }

        /// <summary>
        /// Implements the OnAddInsUpdate method of the IDTExtensibility2 interface. Receives notification
        /// when the collection of Add-ins has changed.
        /// </summary>
        /// <param term='custom'>Array of parameters that are host application specific.</param>
        /// <seealso class='IDTExtensibility2' />
        void IDTExtensibility2.OnAddInsUpdate(ref Array custom)
        {
        }

        /// <summary>
        /// Implements the OnStartupComplete method of the IDTExtensibility2 interface. Receives notification
        /// that the host application has completed loading.
        /// </summary>
        /// <param term='custom'>Array of parameters that are host application specific.</param>
        /// <seealso class='IDTExtensibility2' />
        void IDTExtensibility2.OnStartupComplete(ref Array custom)
        {
        }

        /// <summary>Implements the OnBeginShutdown method of the IDTExtensibility2 interface. Receives notification that the host application is being unloaded.</summary>
        /// <param term='custom'>Array of parameters that are host application specific.</param>
        /// <seealso class='IDTExtensibility2' />
        void IDTExtensibility2.OnBeginShutdown(ref Array custom)
        {
        }
        #endregion

        // To allow the command UI to be cached by VS without even having to load the addin, VS implements a
        // late-binding model where VS calls code:VSInterface.QueryStatus' to determine if a command should be
        // enabled and code:VSInterface.Exec to actually run a command.  
        #region IDTCommandTarget Implementation
        /// <summary>
        /// Implements the QueryStatus method of the IDTCommandTarget interface. This is called when the
        /// command's availability is updated
        /// </summary>
        /// <param term='commandName'>The name of the command to determine state for.</param>
        /// <param term='neededText'>Text that is needed for the command.</param>
        /// <param term='status'>The state of the command in the user interface.</param>
        /// <param term='commandText'>Text requested by the neededText parameter.</param>
        /// <seealso class='Exec' />
        void IDTCommandTarget.QueryStatus(string commandName, vsCommandStatusTextWanted neededText, ref vsCommandStatus status, ref object commandText)
        {
            if (neededText == vsCommandStatusTextWanted.vsCommandStatusTextWantedNone)
            {
                if (commandName.StartsWith(myTypeName) && commandName.Length > myTypeName.Length && commandName[myTypeName.Length] == '.')
                {
                    string commandFunctionName = commandName.Substring(myTypeName.Length + 1);
                    CommandInfo commandInfo;
                    if (addinCommands.TryGetValue(commandFunctionName, out commandInfo))
                    {
                        status = vsCommandStatus.vsCommandStatusSupported;
                        if (commandInfo.Enabled == null || commandInfo.Enabled())
                            status |= vsCommandStatus.vsCommandStatusEnabled;
                    }
                }
            }
        }

        /// <summary>
        /// Implements the Exec method of the IDTCommandTarget interface. This is called when the command is
        /// invoked.
        /// </summary>
        /// <param term='commandName'>The name of the command to execute.</param>
        /// <param term='executeOption'>Describes how the command should be run.</param>
        /// <param term='varIn'>Parameters passed from the caller to the command handler.</param>
        /// <param term='varOut'>Parameters passed from the command handler to the caller.</param>
        /// <param term='handled'>Informs the caller if the command was handled or not.</param>
        /// <seealso class='Exec' />
        void IDTCommandTarget.Exec(string commandName, vsCommandExecOption executeOption, ref object varIn, ref object varOut, ref bool handled)
        {
            handled = false;
            if (executeOption == vsCommandExecOption.vsCommandExecOptionDoDefault)
            {
                if (commandName.StartsWith(myTypeName) && commandName.Length > myTypeName.Length && commandName[myTypeName.Length] == '.')
                {
                    string commandFunctionName = commandName.Substring(myTypeName.Length + 1);
                    CommandInfo commandInfo;
                    if (addinCommands.TryGetValue(commandFunctionName, out commandInfo))
                        commandInfo.Action();
                    else
                        Log.WriteLine("ERROR: Could not find command " + commandFunctionName);
                    handled = true;
                }
            }
        }
        #endregion

        #region IDisposable Members

        public virtual void Dispose()
        {
            // This is here for subclasses to override. 
        }

        #endregion

        #region private methods
        /// <summary>
        /// Sigh.  The indexer for commandBars throws an exception if it is not present.  I don't like
        /// catching these because it means I throw exception in the normal code path, and that screws up my
        /// debugging experience.  Fix this by doing the search myself
        /// </summary>
        private static CommandBar FindCommandBar(string name, CommandBars commandBars)
        {
            foreach (CommandBar cmdBar in commandBars)
            {
                if (cmdBar.Name == name)
                    return cmdBar;
            }
            return null;
        }

        /// <summary>
        /// Sigh.  The indexer for controls throws an exception if it is not present.  I don't like
        /// catching these because it means I throw exception in the normal code path, and that screws up my
        /// debugging experience.  Fix this by doing the search myself
        /// </summary>
        private static CommandBarControl FindControl(string name, CommandBarControls controls)
        {
            foreach (CommandBarControl control in controls)
            {
                if (control.Caption == name)
                    return control;
            }
            return null;
        }

        /// <summary>
        /// Sigh.  The indexer for commands throws an exception if it is not present.  I don't like
        /// catching these because it means I throw exception in the normal code path, and that screws up my
        /// debugging experience.  Fix this by doing the search myself
        /// </summary>
        public static Command FindCommand(string name, Commands2 commands)
        {
            foreach (Command cmd in commands)
            {
                if (cmd.Name == name)
                    return cmd;
            }
            return null;
        }

        #endregion

        #region Private State
        private AddIn me;
        Type myType;                                     // This is typeof(HyperAddin.Connection)
        string myTypeName;                               // This is the literal string HyperAddin.Connection
        Dictionary<string, CommandInfo> addinCommands;   // The commands I implement
        private DTE2 dte;
        #endregion
    }

    /// <summary>
    /// Stuff that depends on the log, but nothing else
    /// </summary>
    public static class Utilities
    {
        /// <summary>
        /// Run a extern process with the command line 'commandLine' sending output to the output window. 
        /// Also return the string that represents the commands output.  'logPrefix' is used to distinguish
        /// this output and is usually the name of the VS command being done. 
        /// </summary>
        public static string RunExternalProcess(string commandLine, string logPrefix)
        {
            Log.WriteLine(logPrefix + ": Running Cmd: " + commandLine);
            try
            {
                Microsoft.Scripting.Command cmd = Microsoft.Scripting.Command.Run(commandLine,
                    new Microsoft.Scripting.CommandOptions().AddTimeout(300000).AddNoThrow());
                Log.Write(cmd.Output);
                if (cmd.ExitCode != 0)
                    Log.WriteLine(logPrefix + ": Command failed.");
                return cmd.Output;
            }
            catch (Exception e)
            {
                Log.WriteLine(logPrefix + ": Command failed.");
                Log.WriteLine("Exception: " + e.Message);
            }
            return "";
        }



        /// <summary>
        /// Tries to copy a embedded resource by the name 'resourceName' into a file 'fileName' returns true
        /// if successful. Resources can be put into a DLL or exe using the
        /// ManifestNonResxWithNoCultureOnDisk Item type in the .csproj file. The LogicalName property is the
        /// 'resourceName' given below. This routine works great for binary files too.
        /// </summary>
        //  Here is an example .csproj directives
        //     <ManifestNonResxWithNoCultureOnDisk Include="Help.htm">
        //     <LogicalName>.\Help.htm</LogicalName>
        //     </ManifestNonResxWithNoCultureOnDisk>
        public static bool UnpackResourceToFile(string resourceName, string fileName)
        {
            Stream stream = System.Reflection.Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName);
            if (stream == null)
                return false;

            byte[] byteBuffer = new byte[1024];
            FileStream outFile = File.Create(fileName);
            for (; ; )
            {
                int byteCount = stream.Read(byteBuffer, 0, byteBuffer.Length);
                if (byteCount == 0)
                    break;
                outFile.Write(byteBuffer, 0, byteCount);
            }
            outFile.Close();
            return true;
        }
    }

    /// <summary>
    /// A stand alone class that allows you to log messages in Visual Studio. You must call 'Initialize' with
    /// the VS dte (Design Time Extensibility object), and the name you wish to give to the output pane (it
    /// is arbitrary).
    /// </summary>
    public static class Log
    {
        /// <summary>
        /// The other functions can be called before Initialize, however they are no-ops until this function
        /// is called
        /// </summary>
        static public void Initialize(DTE2 dte, string paneName)
        {
            _dte = dte;
            _paneName = paneName;
            _enabled = HyperAddinSettings.Default.Logging;
            if (HyperAddinSettings.ErrorMessage != null)
                Log.WriteLine(HyperAddinSettings.ErrorMessage);
        }

        /// <summary>
        /// Flush insures that the text is actually visible to the user. (That is the correct output pane is
        /// open)
        /// </summary>
        static public void Flush()
        {
            if (!_enabled)
                return;

            FlushAlways();
        }

        static public void FlushAlways()
        {
            if (_dte == null)
                return;

            if (_logPane == null)
            {
                _outputWindow = _dte.Windows.Item(EnvDTE.Constants.vsWindowKindOutput);
                _logPane = GetOutputPane(_dte, _paneName);
            }

            Window origWindow = _dte.ActiveWindow;
            _logPane.Activate();
            _outputWindow.Visible = true;
            origWindow.Activate();
        }

        /// <summary>
        /// Write some text to the output log if logging is enabled. 
        /// </summary>
        /// <param name="message">The text to write. </param>
        static public void Write(string message)
        {
            if (!_enabled)
                return;
            WriteAlways(message);
        }

        /// <summary>
        /// Write some text to the log uncontitionally
        /// </summary>
        static public void WriteAlways(string message)
        {
            if (_dte == null)
                return;


            if (_logPane == null)
            {
                _outputWindow = _dte.Windows.Item(EnvDTE.Constants.vsWindowKindOutput);
                _logPane = GetOutputPane(_dte, _paneName);
                Flush();        // Also bring up the right pane (which seems to be expensive).  
            }

            _logPane.OutputString(message);

            // Activate the logging pane to insure it changes to that pane, but then put the focus 
            // back where it was so as not to be annoying. 
            Window w = _dte.ActiveWindow;
            _outputWindow.Visible = true;
            w.Activate();
        }

        /// <summary>
        /// Write some text to the output log, followed by a newline if logging is enabled
        /// </summary>
        /// <param name="message">The text to write. </param>
        static public void WriteLine(string message)
        {
            Write(message + "\r\n");
        }

        /// <summary>
        /// Write some text to the output log, followed by a newline uncondtionally
        /// 
        /// </summary>
        static public void WriteLineAlways(string message)
        {
            WriteAlways(message + "\r\n");
        }

        /// <summary>
        /// Turn on logging or off (default is on, however config file can set)
        /// </summary>
        static public bool Enabled { get { return _enabled; } set { _enabled = value; } }

        /// <summary>
        /// If you are outputing alot of data, getting the LogPane and doing it directly avoids a lot of GUI
        /// slowdown.  
        /// </summary>
        static public OutputWindowPane LogPane
        {
            get
            {
                if (!_enabled)
                    return null;
                return _logPane;
            }
        }

        #region privates

        static OutputWindowPane GetOutputPane(DTE2 dte, string paneName)
        {
            Window window = _dte.Windows.Item(EnvDTE.Constants.vsWindowKindOutput);
            OutputWindow outputWindow = (OutputWindow)window.Object;
            OutputWindowPanes panes = outputWindow.OutputWindowPanes;
            OutputWindowPane ret = null;
            foreach (OutputWindowPane pane in panes)
            {
                if (pane.Name == paneName)
                {
                    ret = pane;
                    break;
                }
            }
            if (ret == null)
                ret = panes.Add(paneName);

            return ret;
        }

        static bool _enabled;
        static DTE2 _dte;
        static string _paneName;
        static OutputWindowPane _logPane;

        static Window _outputWindow;
        #endregion
    }

    public delegate void ActionNoArg();

    /// <summary>
    /// CommandInfo represents all the information the command needs to give to specify its user interface.  
    /// </summary>
    public class CommandInfo
    {
        public CommandInfo(string fullName, ActionNoArg action)
        {
            this.Name = fullName;
            this.Category = "";
            int idx = fullName.LastIndexOf('.');
            if (idx >= 0)
            {
                this.Name = fullName.Substring(idx + 1);
                this.Category = fullName.Substring(0, idx);
            }
            this.ToolTip = this.Name;
            this.Action = action;
        }
        /// <summary>
        /// The name that will be used from the VS Command window (Ctrl-W A) to invoke the command.
        /// This is the last component of the 'fullName' that was passed to the constructor. 
        /// </summary>
        public string Name;
        /// <summary>
        /// Commands are grouped into categories (which can be multi-leveled separated by '.').
        /// </summary>
        public string Category;
        /// <summary>
        /// The name of the button and the floating tooltip used for the command. Defaults to 'Name'
        /// </summary>
        public string ToolTip;
        /// <summary>
        /// A keyboard shortcut for the command.  For example 'Global::Shift+Alt+C,Alt+I' defines a two
        /// keystroke shortcut.   
        /// </summary>
        public string KeyboardBinding;
        /// <summary>
        /// Action is a deletate that gets called when the User invoked the command.  
        /// </summary>
        public ActionNoArg Action;
        /// <summary>
        /// By default commands show up on the 'Addins' Menu dropdown.  Setting this will also place
        /// it on the Addin Toolbar.  
        /// </summary>
        public bool OnToolBar;
        /// <summary>
        /// By default commands show up on the 'Addins' Menu dropdown.  Setting this will also place
        /// it on the right click context menu of the text window.   
        /// </summary>
        public bool OnContextMenu;
        /// <summary>
        /// If nonNull we call this predicate to determine if the command is enabled. 
        /// </summary>
        public Predicate Enabled;
        public delegate bool Predicate();
    };

}
